/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
#undef DebugDoLayout

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using QUT.Bio.Graphs;
using QUT.Bio.Util;
using QUT.Bio.SilverMap.ObjectModel;

namespace QUT.Bio.SilverMap {

	/// <summary>
	/// This adapter yields the default "SilverMap" graph view.
	/// </summary>

	public class DefaultSilverMapAdapter : IGraphAdapter<SequenceSummary> {
		
		#region Variables
		public const double NodeSize = 9;
		private static readonly Brush normalColour = new SolidColorBrush( Colors.Blue );
		private static readonly Brush selectedColour = new SolidColorBrush( Colors.Green );
		private Comparison<Node<SequenceSummary>> nodeSortProc;
		private IEnumerable<string> sortPropertyNames;
		private double epsilon;
		private List<Node<SequenceSummary>> nodeList = new List<Node<SequenceSummary>>();
		private MapControl map;
		private PopupOverlay overlay;
		#endregion

		#region Constructor

		/// <summary>
		/// Initialises the DefaultSilverMapAdapter.
		/// </summary>
		/// <param name="map">The SilverMap control to which this is attached.</param>
		/// <param name="overlay">A surface that can be used to display alerts and popup windows.</param>
		/// <param name="epsilon">The radius of the inner perimeter of the display torus.</param>
		/// <param name="sortPropertyNames">The names of properties used to rank nodes.</param>

		public DefaultSilverMapAdapter (
			MapControl map,
			PopupOverlay overlay,
			double epsilon,
			IEnumerable<string> sortPropertyNames
		) {
			if ( sortPropertyNames == null ) {
				throw new ArgumentException( "sortPropertyNames may not be null." );
			}

			this.map = map;
			this.overlay = overlay;
			this.epsilon = epsilon;
			this.sortPropertyNames = sortPropertyNames;
			nodeSortProc = CompareNodes;
		}

		#endregion
		
		#region Property: SortPropertyNames
		/// <summary>
		/// Get or set the list of property names used to order nodes.
		/// <para>Setter contract:</para>
		/// <para>Pre: sortPropertyNames != null</para>
		/// </summary>
		/// <exception cref="ArgumentException">If value is null.</exception>

		public IEnumerable<string> SortPropertyNames {
			get {
				return sortPropertyNames;
			}
			set {
				if ( value == null ) {
					throw new ArgumentException( "sortPropertyNames may not be null." );
				}

				sortPropertyNames = value;
			}
		}
		#endregion

		#region Method: GetNodeColour

		/// <summary>
		/// Gets a colour for 
		/// </summary>
		/// <param name="node"></param>
		/// <returns></returns>

		protected virtual Brush GetNodeColour ( Node<SequenceSummary> node ) {
			if ( node.HasProperty( "Selected" ) ) {
				object selected = node["Selected"];

				if ( selected is bool ) {
					return (bool) selected ? selectedColour : normalColour;
				}
				else {
					return normalColour;
				}
			}
			else {
				return normalColour;
			}
		}
		#endregion

		#region Method: DoLayout

		/// <summary>
		/// Lays out the list of nodes
		/// <para>
		/// Provides coordinates for each node.
		///		This will be provided with a fully populated Dictionary mapping node id's
		///		to XY pairs.
		///		The function should fill in the X and y coordinates for each node.
		/// </para>
		/// </summary>
		/// <param name="locations">A dictionary mapping nodes to screen coordinates. This should be prepopulated with one point per node.</param>

		public virtual void DoLayout ( Dictionary<SequenceSummary, XY> locations ) {
			int nodeCount = nodeList.Count;

			if ( nodeCount == 0 ) {
				return;
			}

			for ( int i = 0; i < nodeList.Count; i++ ) {
				Node<SequenceSummary> x = nodeList[i];
				SequenceSummary seq = (SequenceSummary) x.Content;
				XY p = locations[seq];

#if DebugDoLayout
				double oldX = p.X;
				double oldY = p.Y;
#endif
	
				if ( map.Model.ReferenceSequence != null && seq.Sequence.Equals( map.Model.ReferenceSequence ) ) {
					p.X = 0;
					p.Y = 0;
				}
				else {			
					double theta = 2 * Math.PI * (double) i / nodeList.Count;
					double distance = map.Model.NormalizedDistanceFromReferenceSequence( seq.Sequence );
					double r = epsilon + ( 1 - epsilon ) * distance;

					p.X = r * Math.Cos( theta );
					p.Y = r * Math.Sin( theta );
				}

#if DebugDoLayout
				Debug.WriteLine( string.Format( "DoLayout: [{0},{1}] -> [{2},{3}]", oldX, oldY, p.X, p.Y ) );
#endif
			}
		}
		#endregion

		#region Method: GetNodeLabel

		/// <summary>
		/// 
		/// </summary>
		/// <param name="node"></param>
		/// <returns></returns>

		public virtual string GetNodeLabel ( Node<SequenceSummary> node ) {
			return node.HasProperty( "Label" ) ? node["Label"].ToString() : node.Content.ToString();
		}

		#endregion

		#region Method: GetNodeShape

		/// <summary>
		/// Creates an ellipse representing a node.
		/// </summary>
		/// <param name="node">A node for which we require </param>
		/// <returns></returns>

		public virtual FrameworkElement GetNodeShape ( Node<SequenceSummary> node ) {
			return new Ellipse {
				Fill = GetNodeColour( node ),
				Width = NodeSize,
				Height = NodeSize
			};
		}
		#endregion

		/// <summary> Called by GraphView when it creates a NodeView, allowing the consumer to
		/// attach event handlers etc to a node view.
		/// <para>In this implementation, we hook listeners to NodeEvent and 
		/// nodeView.Node.Content.PropertyChanged, enabling the map to handle user 
		/// interaction.</para>
		/// </summary>
		/// <param name="nodeView">The glyph used to represent a node.</param>

		public virtual void PrepareNodeView ( NodeView<SequenceSummary> nodeView ) {
			nodeView.NodeEvent += NodeEventHandler;
			( nodeView.Node.Content as SequenceSummary ).PropertyChanged += hitSummary_PropertyChanged;
		}

		/// <summary> Orders nodes using the list of property names provided when the adapter was created.
		/// </summary>
		/// <param name="x">One item to compare.</param>
		/// <param name="y">Another item to compare.</param>
		/// <returns>A standard comparison result value.</returns>

		private int CompareNodes ( Node<SequenceSummary> x, Node<SequenceSummary> y ) {
			int comparison = 0;

			foreach ( string propertyName in sortPropertyNames ) {
				if ( comparison != 0 ) {
					break;
				}

				comparison = ( x[propertyName] as IComparable ).CompareTo( y[propertyName] );
			}

			return comparison;
		}

		/// <summary> Called by the GraphView NodesChanged event handler.
		/// <para>
		///		This adapeter maintains a sorted list of nodes that is used to lay them out.
		///		We replace the contents of the node list and sort it to support the angular 
		///		ordering required by the layout manager.
		/// </para>
		/// </summary>
		/// <param name="nodes">A list of nodes that will be displayed by DoLayout.</param>

		public virtual void UpdateNodes ( IEnumerable<Node<SequenceSummary>> nodes ) {
			nodeList.Clear();
			nodeList.AddRange( nodes );
			nodeList.Sort( CompareNodes );
		}

		#region Method: NodeEventHandler
		/// <summary>
		/// Responds to user interactions with nodes.
		/// </summary>
		/// <param name="nodeView"></param>
		/// <param name="action"></param>

		protected virtual void NodeEventHandler ( NodeView<SequenceSummary> nodeView, NodeAction action ) {
			Node<SequenceSummary> node = nodeView.Node;
			SequenceSummary sequenceSummary = (SequenceSummary) node.Content;

			switch ( action ) {
				case NodeAction.LabelClicked:
					double x = nodeView.MouseX;
					double y = nodeView.MouseY;

					SequencePropertyPanel details = map.PageLayout.SequencePropertyPanel;
					details.Sequence = node.Content.Sequence;
					map.PageLayout.Show( details );
					map.PageLayout.Alignment.SelectedSequence = sequenceSummary.Sequence;
					break;

				case NodeAction.NodeClicked:
					sequenceSummary.Selected = ! sequenceSummary.Selected;
					break;

				case NodeAction.NodeDoubleClicked:
					sequenceSummary.Selected = true;
					map.Model.ReferenceSequence = sequenceSummary.Sequence;
					break;
			}

		}
		#endregion

		#region Method: hitSummary_PropertyChanged
		/// <summary>
		/// Updates the fill colour when a node is selected or deselected.
		/// <para>Pre: sender is Gene.</para>
		/// <para>Post: args.PropertyName is "Selected" and vertex has been updated to reflect the current 
		/// selected state of the sending sequenceSummary; or args.PropertyName is not selected and nothing has changed.</para>
		/// </summary>
		/// <param name="sender">The object that originated the event, which we assume to be the sequenceSummary in question.</param>
		/// <param name="args">Carries the name of the altered property.</param>

		private void hitSummary_PropertyChanged ( object sender, PropertyChangedEventArgs args ) {
			SequenceSummary sequence = sender as SequenceSummary;

			if ( args.PropertyName == "Selected" ) {
				// This is grotesque.
				FrameworkElement glyph = map.View.GraphView.NodeViews[sequence].LabelledVertex.Glyph;
				PropertyInfo property = glyph.GetType().GetProperty( "Fill" );
				if ( property != null ) {
					try {
						property.SetValue( glyph, sequence.Selected ? selectedColour : normalColour, null );
					}
					catch ( Exception e ) {
						Debug.WriteLine( string.Format( "Setting fill failed:\n{0}", e ) );
					}
				}
			}
		}
		#endregion
	}
}
